<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="root">
    <div class="c1">
      <div title="title1">name: {{obj.name}}</div>
      <div title="title2">age: {{obj.age}}</div>
      <div title="title3">sex: {{obj.sex}}</div>
      <ul>
        <li>{{name}}</li>
        <li>{{age}}</li>
        <li>{{sex}}</li>
      </ul>
    </div>
  </div>

  <script>
    /*
   发布订阅模式
   问题：解决，赋值的时候将数据进行响应式化，赋值即Object.defineProperty.set(),
   将数据响应式化reactify(o,vm)
   vue源码中的数组也不是响应式的。
   从我们定义的reactify也可以看出
   1.对于data中的数组如果重新定义为一个对象，则此对象不是响应式的
   2.data中新增的对象也不是响应式。

   代理方法(app.name, app._data.name)
    将app._data中的成员给映射到app上，由于需要在更新数据的时候，更新页面的内容
    所以app._data访问的成员与app访问的成员应该是同一个成员，
    当app._data改变的时候页面刷新，app改变的时候页面也刷新，
    如果不是同一个成员，这两个数据需要同步，复杂性会提高
    由于app._data已经是响应式对象，所以只需要让app访问的成员去访问app._data成员即可
    app.name 转换为 app._data.name
    vue引入了一个函数proxy(target, src, prop),target:app,src:app._data,prop:name
    将target的操作映射到src.prop上，这里因为当时没有Proxy语法（es6）
    页面中第一次处理data是在JGVue的构造函数中，reactify(o,vm)方法上，
    所以在reactify(o,vm)上添加proxy函数

   事件模型(node: event 模块)
   vue中的observe和watcher和dep
   */
    function defineReactive(target, key, value, enumerable) {
      let that = this;
      if (typeof value === 'object' && value != null && !Array.isArray(value)) {
        reactify(value, that);
      }
      Object.defineProperty(target, key, {
        configurable: true,
        enumerable: !!enumerable,
        get() {
          console.log('读取o的' + key + '属性');
          return value
        },
        set(newVal) {
          console.log('设置o的' + key + '属性为：' + newVal);
          // 临时过渡一下，判断是不是对象
          if(typeof newVal === 'object' && newVal != null) {
            value = reactify(newVal /*, vm实例*/);
          } else {
            value = newVal;
          }
          that.mountComponent(); // 更新页面，写死了，不够灵活

        }
      })
    }

    /*
    我们之前处理的reactify方法，需要改写一下
    提供一个Observer的方法，vue源代码的方法，在这个方法中对属性进行处理
    可以将方法封装到initData方法中
    */
    function reactify(o, vm) {
      let keys = Object.keys(o)

      for (let i = 0; i < keys.length; i++) {
        let key = keys[i];
        let value = o[key]
        if (Array.isArray(value)) {
          /*
          如果data中的数据是数组的话，会对数组中的元素value进行处理，
          else是如果不是数组的话进行响应式，
          而没有专门的针对数组进行响应式，所以vue2.0的源码中的数组不是
          响应式的。
          */
          value.__proto__ = array_methods;
          for (let j = 0; j < value.length; j++) {
            reactify(value[j], vm);
          }
        } else {
          defineReactive.call(vm, o, key, value, true)
        }

        /*
        只需要在这里添加代理即可（问题： 在这里写的代码会是递归）
        如果在这里将属性映射到vue实例上，那么就表示vue实例可以使用属性key，
        由于这里都是递归，
        如果嵌套层级太多，有相同属性的名称，则后边的name会覆盖前边的name，
        {
         data: {
           name: ...,
           children: {
             name: ...,
            }
          }
        }
        */
       
      }
    }

    /*
    observer取代reactify，将对象转换为响应式
    在observer中调用defineReactive方法
    在observer查看对象的成员，递归
    先递归判断成员在defineReactive
    */
    function observer(obj, propName, vm) {
      let keys = Object.keys(obj);
      for(let i=0;i<keys.length;i++) {
        // 判断是数组还是对象
        let key = keys[i];
        if(Array.isArray(obj[propName])) {
          // 遍历成员
          for(let j=0;j<keys.length;j++) {
            // 将obj[key][j]变成响应式的
          observer(obj[key][j], vm)
          }
        } else {
          // 递归，将obj[key] 变成响应式的
          observer(obj[key], vm)
        }
      }

      // 开始对obj调用defineReactive方法
      defineReactive(obj, )
    }

    let ARRAY_METHOD = [
      "push",
      "pop",
      "shift",
      "unshift",
      "sort",
      "splice"
    ]

    let array_methods = Object.create(Array.prototype);
    ARRAY_METHOD.forEach(method => {
      array_methods[method] = function () {
        console.log('调用的是拦截的' + method + '方法');
        for (let i = 0; i < arguments.length; i++) {
          reactify(arguments[i])
        }

        let res = Array.prototype[method].apply(this, arguments)
        return res;
      }
    })

    class VNode {
      constructor(tag, data, value, type) {
        this.tag = tag && tag.toLowerCase();
        this.data = data;
        this.value = value;
        this.type = type;
        this.children = [];
      }
      appendChild(vnode) {
        this.children.push(vnode)
      }
    }
    
    function getVNode(node) {
      let nodeType = node.nodeType;
      let _vnode = null;
      if (nodeType === 1) {
        let nodeName = node.nodeName;
        let attrs = node.attributes;
        let _attrObj = {};
        for (let i = 0; i < attrs.length; i++) {
          _attrObj[attrs[i].nodeName] = attrs[i].nodeValue;
        }
        _vnode = new VNode(nodeName, _attrObj, undefined, nodeType)

        let childNodes = node.childNodes;
        for (let i = 0; i < childNodes.length; i++) {
          _vnode.appendChild(getVNode(childNodes[i]))
        }

      } else if (nodeType === 3) {
        _vnode = new VNode(undefined, undefined, node.nodeValue, nodeType)
      }
      return _vnode
    }
    
    function parseVNode(vnode) {
      let type = vnode.type;
      let _node = null;
      if (type === 3) {
        return document.createTextNode(vnode.value);
      } else if (type === 1) {
        _node = document.createElement(vnode.tag);

        let data = vnode.data;
        Object.keys(data).forEach(key => {
          let attrName = key;
          let attrValue = data[key];
          _node.setAttribute(attrName, attrValue);
        })
        let children = vnode.children;
        children.forEach(subvnode => {
          _node.appendChild(parseVNode(subvnode))
        })
        return _node;
      }
    }

    let rkuohao = /\{\{(.+?)\}\}/g;

    function getValueByPath(obj, path) {
      let paths = path.split('.');
      let res = obj;
      let prop;
      while (prop = paths.shift()) {
        res = res[prop]
      }
      return res
    }

    function combine(vnode, data) {
      let _type = vnode.type;
      let _data = vnode.data;
      let _value = vnode.value;
      let _tag = vnode.tag;
      let _children = vnode.children;

      let _vnode = null;
      if (_type === 3) {
        _value = _value.replace(rkuohao, function (_, g) {
          return getValueByPath(data, g.trim())
        })
        _vnode = new VNode(_tag, _data, _value, _type)
      } else if (_type === 1) {
        _vnode = new VNode(_tag, _data, _value, _type);
        _children.forEach(_subvnode => _vnode.appendChild(combine(_subvnode, data)))
      }

      return _vnode;
    }

    function JGVue(options) {
      this._data = options.data;
      this.elm = document.querySelector(options.el);
      this._template = this.elm;
      this._parent = this.elm.parentNode;
      
      this.initData(); // 将data进行响应式转化，进行代理

      this.mount();
    }

    JGVue.prototype.mount = function () {
      this.render = this.createRenderFn()
      this.mountComponent()
    }

    JGVue.prototype.mountComponent = function () {
      let mount = () => {
        this.update(this.render())
      }
      mount.call(this);
    }

    JGVue.prototype.createRenderFn = function () {
      let ast = getVNode(this._template);
      return function render() {
        let _tmp = combine(ast, this._data);
        return _tmp
      }
    }
    JGVue.prototype.update = function (vnode) {
      let realDom = parseVNode(vnode)
      this._parent.replaceChild(realDom, document.querySelector('#root'))
    }

    /* 
    遍历this._data的成员，将属性转换为响应式，将直接属性代理到实例上
    reactify()响应式只针对了对象的属性和数组的值进行了响应式，
    对对象并没有响应式化，需要优化
    */
    JGVue.prototype.initData = function () {
      let keys = Object.keys(this._data);
      // 响应式化，将this._data[keys[i]] 变成响应式的
      for(let i=0;i<keys.length;i++) {
        reactify(this._data, this)
      }

      /*
      代理 将this._data[keys[i]]映射到this[keys[i]]上,
      就是要让this提供keys[i]这个属性，
      在访问这个属性的时候相当于在访问this._data的这个属性
      */
      for(let i=0;i<keys.length;i++) {
        // Object.defineProperty(this, keys[i], {
        //   enumerable: true,
        //   configurable: true,
        //   set(newVal) {
        //     this._data[keys[i]] = newVal;
        //   },
        //   get() {
        //     return this._data[keys[i]]
        //   }
        // })

        proxy(this, '_data', keys[i])
      }
    }

    /*
    将某一个对象的属性访问映射到对象的某一个属性成员上
    把target这个对象去访问key属性，映射到，这个对象target的prop属性的key上
    代理：
    之前访问是app._data,所有的属性都挂在到这里，根据vue设计，不希望访问_开头的数据，
    vue中$开头只读，_开头是私有数据，尽量不要访问
    习惯性访问app.xxx，直接映射到app._data.xxx

    */
    function proxy(target, prop, key) {
      Object.defineProperty(target, key, {
          enumerable: true,
          configurable: true,
          set(newVal) {
            target[prop][key] = newVal;
          },
          get() {
            return target[prop][key]
          }
        })

    }

    let app = new JGVue({
      el: '#root',
      data: {
        name: '张',
        age: 'dan',
        sex: '女',
        obj: {
          name: 'obj的name',
          age: 'obj的age',
          sex: 'obj的sex'
        }
      }
    })


    /*
    发布订阅模式
    目的：解耦，让各个模块直接没有那么强的依赖性
    现在的办法是属性在更新的时候调用mountComponent方法，
    mountComponent更新的是什么东西？？？
    更新的是全部的页面，当前虚拟dom对应的页面dom，不符合vue的特征，真个dom的替换
    在vue中整个的更新是按照组件为单位进行判断，以节点为单位进行更新
    如果代码中没有自定义组件，那么在比较算法的时候，我们会将
    全部的模板对应的虚拟dom进行比较，如果含有自定义组件，
    在比较的时候就会判断更新的是哪一些组件中的属性，只会判断更新
    数据的组件，其他组件不会更新。
    复杂的页面是有很多组件构成的，每一个属性要更新的时候都要去
    调用更新的方法，就会有一个问题。
    目标：如果修改了什么属性，就尽可能只更新这些属性对应的页面的dom
    这样就一定不能将更新的代码写死。


    */


  </script>
</body>

</html>